﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.Data;
using Orchard.Environment.Extensions;
using Orchard.Mvc.Extensions;
using Orchard.Services;
using System.Web.Routing;
using System.Web.Mvc;
using Orchard;
using Downplay.Audit.Models;

namespace Downplay.Audit.Services
{
    public class AuditService : IAuditService
    {
        private readonly IRepository<AuditRecord> _auditRepository;
        private readonly IClock _clock;
        private readonly RequestContext _requestContext;
        private readonly IRepository<UserLocationRecord> _userLocationRepository;
        private readonly IUserLocationEvents _userLocationEvents;

        public const string UserLocationIdSessionKey = "Downplay_Audit_Services_AuditService_UserLocationId";
        public const string UserLocationIdCookieKey = "Downplay_Audit_Services_AuditService_UserLocationId";

        public IOrchardServices Services { get; set; }

        public AuditService(
            IOrchardServices services,
            IRepository<AuditRecord> auditRepository,
            IClock clock,
            RequestContext requestContext,
            IRepository<UserLocationRecord> userLocationRepository,
            IUserLocationEvents userLocationEvents
            )
        {
            Services = services;
            _auditRepository = auditRepository;
            _clock = clock;
            // TODO: When in background process / console we don't have an RC so IP is mostly meaningless
            // in those cases we could assume "localhost" perhaps...
            _requestContext = requestContext;
            _userLocationRepository = userLocationRepository;
            _userLocationEvents = userLocationEvents;
        }

        public AuditRecord GetAudit(long auditId) {
            return _auditRepository.Get(a=>a.Id == auditId);
        }

        public AuditRecord CreateAudit(string auditCode, Orchard.ContentManagement.IContent content = null, bool confirmed = true, string data = "")
        {
            var audit = new AuditRecord()
            {
                TimeInitiated = _clock.UtcNow,
                UserLocationId = CurrentUserLocation().Id,
                AuditCode = auditCode,
                AuditData = data
            };

            if (_requestContext != null && _requestContext.HttpContext != null)
            {
                audit.SourceUrl = _requestContext.HttpContext.Request.Url.AbsoluteUri;
                if (_requestContext.HttpContext.Request.UrlReferrer != null)
                {
                    audit.ReferrerUrl = _requestContext.HttpContext.Request.UrlReferrer.AbsoluteUri;
                }
            }
            // Allow null content
            if (content!=null) {
                audit.ContentItemId = content.ContentItem.Id;
                if (content.ContentItem.VersionRecord!=null) {
                    audit.ContentItemVersionId = content.ContentItem.VersionRecord.Id;
                }
            }
            if (confirmed)
            {
                audit.TimeCompleted = _clock.UtcNow;
            }
            _auditRepository.Create(audit);
            _auditRepository.Flush();
            return audit;
        }

        public bool CompleteAudit(long auditId)
        {
            // Check log exists and is valid for current user
            var log = GetAudit(auditId);
            if (log == null) return false;
            if (CurrentUserLocation().Id != log.UserLocationId) return false;

            // Confirm it
            log.TimeCompleted = _clock.UtcNow;
            return true;
        }

        public string AuditCompletionUrl(AuditRecord audit)
        {
            var url = new UrlHelper(_requestContext);
            return url.AbsoluteAction("Complete", "Audit", new { area = "Downplay.Audit", id = audit.Id });
        }

        public UserLocationRecord CurrentUserLocation()
        {
            if (_requestContext==null) return null;

            UserLocationRecord currentDetails = null;
            var now = _clock.UtcNow;

            // Read location from session / cookie
            // TODO: How do cookies work? Are they invalidated when a user logs in or out? (This would make some of the following logic completely redundant)
            // ... Should be easy to test in debug

            // TODO: Time out at intervals and recheck even if it's stored in session?
                
            var locId = _requestContext.HttpContext.Session[UserLocationIdSessionKey];

            if (locId == null) {
                // Check for a cookie
                // TODO: This creates a problem if someone e.g. takes their laptop abroad. Need to restructure this function a bit.
                var cookie = _requestContext.HttpContext.Request.Cookies[UserLocationIdCookieKey];
                if (cookie!=null)
                    locId = cookie.Value;
            }

            long id;
            if (locId != null && Int64.TryParse(locId.ToString(), out id)) {
                // Note: We can't Get(id) because it's long and Get() only supports int
                currentDetails = _userLocationRepository.Get(ulr => ulr.Id == id);
            }

            // If we have details, verify it matches the user
            if (currentDetails != null) {
                var user = Services.WorkContext.CurrentUser;
                if (user != null) {
                    if (currentDetails.UserId.HasValue) {
                        if (currentDetails.UserId.Value != user.Id) {
                            // Session/cookie had a different user to the one logged in
                            currentDetails = null;
                        }
                    }
                    else {
                        // Session/cookie had no user but one is logged in
                        currentDetails = null;
                    }
                }
                else if (currentDetails.UserId.HasValue) {
                    // Session/cookie had a user but no user is logged in
                    currentDetails = null;
                }
            }
            if (currentDetails == null) {
                // Find or create location record for user and request
                currentDetails = CreateUserLocationData();
                
                // Check if we already have a location record for this user that matches current details.
                int? userId = currentDetails.UserId;
                // Get last seen matching location for user
                var locQ = from l in _userLocationRepository.Table
                           where l.UserId == userId
                           && l.IPv4Address == currentDetails.IPv4Address
                           && l.IPv6Address == currentDetails.IPv6Address
                           orderby l.LastSeen descending
                           select l;

                // Drop to enumerated list because SQL can't compare ntext fields. This could get slow sometimes and an SPROC or some other method might be better.
                var test = locQ.ToList().Where(l => l.BrowserDetails == currentDetails.BrowserDetails
                           && l.HostDetails == currentDetails.HostDetails).FirstOrDefault();

                if (test != null) {
                    currentDetails = test;
                }
                else {
                    currentDetails.FirstSeen = currentDetails.LastChecked = currentDetails.LastSeen = now;
                    _userLocationRepository.Create(currentDetails);
                    _userLocationEvents.Created(currentDetails);
                }
                _requestContext.HttpContext.Response.SetCookie(new HttpCookie(UserLocationIdCookieKey, currentDetails.Id.ToString()));
            }

            // Update last seen
            currentDetails.LastSeen = now;

            // Fire events; 
            _userLocationEvents.Seen(currentDetails);

            // Store location id for life of session
            _requestContext.HttpContext.Session[UserLocationIdSessionKey] = currentDetails.Id;
            return currentDetails;
        }

        protected UserLocationRecord CreateUserLocationData()
        {
            // Choose whether host is IPv4 or IPv6
            var httpContext = Services.WorkContext.HttpContext;
            var hostAdd = httpContext.Request.UserHostAddress;
            string ipv4add = "", ipv6add = "";
            if (hostAdd.Length < 16 && hostAdd.Split('.').Count() == 4)
            {
                ipv4add = hostAdd;
            }
            else
            {
                ipv6add = hostAdd;
            }

            // Create location data
            var data = new UserLocationRecord()
            {
                BrowserDetails = httpContext.Request.UserAgent,
                HostDetails = httpContext.Request.UserHostName,
                IPv4Address = ipv4add,
                IPv6Address = ipv6add
            };
            if (Services.WorkContext.CurrentUser == null)
            {
                data.UserId = null;
            }
            else
            {
                data.UserId = Services.WorkContext.CurrentUser.Id;
            }
            return data;
        }

    }
}